﻿/*
 * This is a .NET port of the Apache Chemistry QueryParser. http://chemistry.apache.org/
 * The conversion was performed by Brügmann Software GmbH. Programmer: Björn Kremer.
 *
 * This project is still under the original Apache License: 
 * 
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using QueryParser;

namespace QueryParser
{


   using Tree = Antlr.Runtime.Tree.ITree;
   using TypeDefinition = CMISObjectModel.TypeDefinitions.TypeDefinition;
   using TypeManager = QueryParser.ITypeManager;

   


   /// <summary>
   /// QueryObject is a class used to encapsulate a CMIS query. It is created from
   /// an ANTLR parser on an incoming query string. During parsing various
   /// informations are collected and stored in objects suitable for evaluating the
   /// query (like selected properties, effected types and order statements. A query
   /// evaluator can use this information to perform the query and build the result.
   /// </summary>
   public class QueryObject
   {

    //  private static readonly Logger LOG = LoggerFactory.getLogger(typeof(QueryObject));

      // For error handling see:
      // http://www.antlr.org/pipermail/antlr-interest/2008-April/027600.html
      // select part
      protected internal TypeManager typeMgr;
      protected internal readonly IList<CmisSelector> selectReferences = new List<CmisSelector>();
      protected internal readonly IList<CmisSelector> whereReferences = new List<CmisSelector>();
      protected internal readonly IList<CmisSelector> joinReferences = new List<CmisSelector>();
      // --> Join not implemented yet
      protected internal readonly IDictionary<string, CmisSelector> colOrFuncAlias = new Dictionary<string, CmisSelector>();

      // from part
      /// <summary>
      /// map from alias name to type query name </summary>
      protected internal readonly IDictionary<string, string> froms = new System.Collections.Generic.Dictionary<string, string>();

      /// <summary>
      /// main from alias name </summary>
      protected internal string from = null;

      protected internal readonly IList<JoinSpec> joinSpecs = new List<JoinSpec>();

      // where part
      protected internal readonly IDictionary<int?, CmisSelector> columnReferences = new Dictionary<int?, CmisSelector>();
      protected internal readonly IDictionary<int?, string> typeReferences = new Dictionary<int?, string>();

      // order by part
      protected internal readonly IList<SortSpec> sortSpecs = new List<SortSpec>();

      private string errorMessage;

      public class JoinSpec
      {

         /// <summary>
         /// INNER / LEFT / RIGHT </summary>
         public readonly string kind;

         /// <summary>
         /// Alias or full table type </summary>
         public readonly string alias;

         public ColumnReference onLeft;

         public ColumnReference onRight;

         public JoinSpec(string kind, string alias)
         {
            this.kind = kind;
            this.alias = alias;
         }

         public virtual void setSelectors(ColumnReference onLeft, ColumnReference onRight)
         {
            this.onLeft = onLeft;
            this.onRight = onRight;
         }

         public override string ToString()
         {
            return "JoinReference(" + kind + "," + alias + "," + onLeft + "," + onRight + ")";
         }
      }

      public class SortSpec
      {
         private readonly QueryObject outerInstance;

         public readonly bool ascending;
         public readonly int? colRefKey; // key in columnReferencesMap point to column
         // descriptions

         public SortSpec(QueryObject outerInstance, int? key, bool ascending)
         {
            this.outerInstance = outerInstance;
            this.colRefKey = key;
            this.ascending = ascending;
         }

         public virtual CmisSelector Selector
         {
            get
            {
               return outerInstance.columnReferences[colRefKey];
            }
         }

         public virtual bool Ascending
         {
            get
            {
               return ascending;
            }
         }
      }

      public QueryObject()
      {
      }

      public QueryObject(TypeManager tm)
      {
         typeMgr = tm;
      }

      public virtual IDictionary<int?, CmisSelector> ColumnReferences
      {
         get
         {
            return new System.Collections.Generic.Dictionary<int?, CmisSelector>(columnReferences);
         }
      }

      public virtual CmisSelector getColumnReference(int? token)
      {
         return columnReferences[token];
      }

      public virtual string getTypeReference(int? token)
      {
         return typeReferences[token];
      }

      public virtual string ErrorMessage
      {
         get
         {
            return errorMessage;
         }
      }

      // ///////////////////////////////////////////////////////
      // SELECT part

      // public accessor methods
      public virtual IList<CmisSelector> SelectReferences
      {
         get
         {
            return selectReferences;
         }
      }

      public virtual void addSelectReference(Tree node, CmisSelector selRef)
      {
         selectReferences.Add(selRef);
         columnReferences[node.TokenStartIndex] = selRef;
      }

      public virtual void addAlias(string aliasName, CmisSelector aliasRef)
      {
         //LOG.debug("add alias: " + aliasName + " for: " + aliasRef);
         if (colOrFuncAlias.ContainsKey(aliasName))
         {
            throw new CmisQueryException("You cannot use name " + aliasName + " more than once as alias in a select.");
         }
         else
         {
            aliasRef.AliasName = aliasName;
            colOrFuncAlias[aliasName] = aliasRef;
         }
      }

      public virtual CmisSelector getSelectAlias(string aliasName)
      {
         return colOrFuncAlias[aliasName];
      }

      // ///////////////////////////////////////////////////////
      // FROM part

      public virtual string addType(string aliasName, string typeQueryName)
      {
         try
         {
            if (aliasName == null)
            {
               aliasName = typeQueryName;
            }
            //LOG.debug("add alias: " + aliasName + " for: " + typeQueryName);
            if (froms.ContainsKey(aliasName))
            {
               throw new CmisQueryException("You cannot use name " + aliasName + " more than once as alias in a from part.");
            }
           
            froms[aliasName] = typeQueryName;
            if (from == null)
            {
               from = aliasName;
            }
            return aliasName;
         }
         catch (CmisQueryException cqe)
         {
            errorMessage = cqe.Message; // preserve message
            return null; // indicate an error to ANTLR so that it generates FailedPredicateException
         }
      }

      public virtual string MainTypeAlias
      {
         get
         {
            return from;
         }
      }

      public virtual IDictionary<string, string> Types
      {
         get
         {
            return new System.Collections.Generic.Dictionary<string, string>(froms);
         }
      }

      public virtual string getTypeQueryName(string qualifier)
      {
         return froms[qualifier];
      }

      public virtual TypeDefinition getTypeDefinitionFromQueryName(string queryName)
      {
         return typeMgr.GetTypeByQueryName(queryName);
      }

      //public virtual TypeDefinition getParentType(TypeDefinition td)
      //{
      //   string parentType = td.ParentTypeId;
      //   return parentType == null ? null : typeMgr.getTypeById(parentType).TypeDefinition;
      //}

      //public virtual TypeDefinition getParentType(string typeId)
      //{
      //   TypeDefinition td = typeMgr.getTypeById(typeId).TypeDefinition;
      //   string parentType = td == null ? null : td.ParentTypeId;
      //   return parentType == null ? null : typeMgr.getTypeById(parentType).TypeDefinition;
      //}

      public virtual TypeDefinition MainFromName
      {
         get
         {
            // as we don't support JOINS take first type
            var enumerator = froms.Values.GetEnumerator();
            enumerator.MoveNext();
            string queryName = enumerator.Current;
            TypeDefinition td = getTypeDefinitionFromQueryName(queryName);
            return td;
         }
      }

      /// <summary>
      /// return a map of all columns that have been requested in the SELECT part
      /// of the statement.
      /// </summary>
      /// <returns> a map with a String as a key and value. key is the alias if 
      /// an alias was given or the query name otherwise. value is the query 
      /// name of the property. </returns>
      public virtual IDictionary<string, string> RequestedPropertiesByAlias
      {
         get
         {
            return getRequestedProperties(true);
         }
      }

      /// <summary>
      /// return a map of all columns that have been requested in the SELECT part
      /// of the statement.
      /// </summary>
      /// <returns> a map with a String as a key and value. key is the query name of
      ///         the property, value is the alias if an alias was given or the
      ///         query name otherwise.
      /// </returns>
      /// @deprecated  Use getRequestedPropertiesByAlias instead. 
      [Obsolete(" Use getRequestedPropertiesByAlias instead.")]
      public virtual IDictionary<string, string> RequestedProperties
      {
         get
         {
            return getRequestedProperties(false);
         }
      }

      private IDictionary<string, string> getRequestedProperties(bool byAlias)
      {

         IDictionary<string, string> res = new Dictionary<string, string>();
         foreach (CmisSelector sel in selectReferences)
         {
            if (sel is ColumnReference)
            {
               ColumnReference colRef = (ColumnReference)sel;
               string key = colRef.PropertyId;
               if (null == key)
               {
                  key = colRef.PropertyQueryName; // happens for *
               }
               string propDescr = colRef.AliasName == null ? colRef.PropertyQueryName : colRef.AliasName;
               if (byAlias)
               {
                  res[propDescr] = key;
               }
               else
               {
                  res[key] = propDescr;
               }
            }
         }
         return res;
      }

      /// <summary>
      /// return a map of all functions that have been requested in the SELECT part
      /// of the statement.
      /// </summary>
      /// <returns> a map with a String as a key and value. key is the function name
      ///         of the property, value is the alias if an alias was given or the
      ///         function name otherwise.
      /// </returns>
      /// @deprecated  Use getRequestedPropertiesByAlias instead. 
      [Obsolete(" Use getRequestedPropertiesByAlias instead.")]
      public virtual IDictionary<string, string> RequestedFuncs
      {
         get
         {
            return getRequestedFuncs(false);
         }
      }

      /// <summary>
      /// return a map of all functions that have been requested in the SELECT part
      /// of the statement.
      /// </summary>
      /// <returns> a map with a String as a key and value. key is the alias if an 
      /// alias was given or the function name otherwise, value is the a name
      /// of the property.  </returns>
      public virtual IDictionary<string, string> RequestedFuncsByAlias
      {
         get
         {
            return getRequestedFuncs(true);
         }
      }

      private IDictionary<string, string> getRequestedFuncs(bool byAlias)
      {

         IDictionary<string, string> res = new Dictionary<string, string>();
         foreach (CmisSelector sel in selectReferences)
         {
            if (sel is FunctionReference)
            {
               FunctionReference funcRef = (FunctionReference)sel;
               string propDescr = funcRef.AliasName == null ? funcRef.Name : funcRef.AliasName;
               if (byAlias)
               {
                  res[propDescr] = funcRef.Name;
               }
               else
               {
                  res[funcRef.Name] = propDescr;
               }
            }
         }
         return res;
      }

      // ///////////////////////////////////////////////////////
      // JOINS

      public virtual void addJoinReference(Tree node, CmisSelector reference)
      {
         columnReferences[node.TokenStartIndex] = reference;
         joinReferences.Add(reference);
      }

      public virtual IList<CmisSelector> JoinReferences
      {
         get
         {
            return new System.Collections.ObjectModel.ReadOnlyCollection<CmisSelector>(joinReferences);
         }
      }

      public virtual void addJoin(string kind, string alias, bool hasSpec)
      {
         JoinSpec join = new JoinSpec(kind, alias);
         if (hasSpec)
         {
            // get columns from last added references
            int n = joinReferences.Count;
            ColumnReference onLeft = (ColumnReference)joinReferences[n - 2];
            ColumnReference onRight = (ColumnReference)joinReferences[n - 1];
            join.setSelectors(onLeft, onRight);
         }
         joinSpecs.Add(join);
      }

      public virtual IList<JoinSpec> Joins
      {
         get
         {
            return joinSpecs;
         }
      }

      // ///////////////////////////////////////////////////////
      // WHERE part


      public virtual void addWhereReference(Tree node, CmisSelector reference)
      {
         //LOG.debug("add node to where: " + System.identityHashCode(node));
         columnReferences[node.TokenStartIndex] = reference;
         whereReferences.Add(reference);
      }

      public virtual IList<CmisSelector> WhereReferences
      {
         get
         {
            return new System.Collections.ObjectModel.ReadOnlyCollection<CmisSelector>(whereReferences);
         }
      }

      public virtual void addWhereTypeReference(Tree node, string qualifier)
      {
         if (node != null)
         {
            typeReferences[node.TokenStartIndex] = qualifier;
         }
      }

      // ///////////////////////////////////////////////////////
      // ORDER_BY part

      public virtual IList<SortSpec> OrderBys
      {
         get
         {
            return new System.Collections.ObjectModel.ReadOnlyCollection<SortSpec>(sortSpecs);
         }
      }

      public virtual void addSortCriterium(Tree node, ColumnReference colRef, bool ascending)
      {
         //LOG.debug("addSortCriterium: " + colRef + " ascending: " + ascending);
         columnReferences[node.TokenStartIndex] = colRef;
         sortSpecs.Add(new SortSpec(this, node.TokenStartIndex, ascending));
      }

      // ///////////////////////////////////////////////////////
      // resolve types after first pass traversing the AST is complete

      public virtual bool resolveTypes()
      {
         try
         {
            //LOG.debug("First pass of query traversal is complete, resolving types");
            if (null == typeMgr)
            {
               return true;
            }

            // First resolve all alias names defined in SELECT:
            foreach (CmisSelector alias in colOrFuncAlias.Values)
            {
               if (alias is ColumnReference)
               {
                  ColumnReference colRef = ((ColumnReference)alias);
                  resolveTypeForAlias(colRef);
               }
            }

            // Then replace all aliases used somewhere by their resolved column
            // reference:
            foreach (int? obj in columnReferences.Keys)
            {
               CmisSelector selector = columnReferences[obj];
               string key = selector.Name;
               if (colOrFuncAlias.ContainsKey(key)) // it is an alias
               {
                  CmisSelector resolvedReference = colOrFuncAlias[key];
                  columnReferences[obj] = resolvedReference;
                  // Note: ^ This may replace the value in the map with the same
                  // value, but this does not harm.
                  // Otherwise we need to check if it is resolved or not which
                  // causes two more ifs:
                  // if (selector instanceof ColumnReference) {
                  // ColumnReference colRef = ((ColumnReference) selector);
                  // if (colRef.getTypeDefinition() == null) // it is not yet
                  // resolved
                  // // replace unresolved column reference by resolved on from
                  // alias map
                  // columnReferences.put(obj,
                  // colOrFuncAlias.get(selector.getAliasName()));
                  // } else
                  // columnReferences.put(obj,
                  // colOrFuncAlias.get(selector.getAliasName()));
                  if (whereReferences.Remove(selector))
                  {
                     // replace unresolved by resolved reference
                     whereReferences.Add(resolvedReference);
                  }
                  if (joinReferences.Remove(selector))
                  {
                     // replace unresolved by resolved reference
                     joinReferences.Add(resolvedReference);
                  }
               }
            }

            // The replace all remaining column references not using an alias
            foreach (CmisSelector select in columnReferences.Values)
            {
               // ignore functions here
               if (select is ColumnReference)
               {
                  ColumnReference colRef = ((ColumnReference)select);
                  if (colRef.TypeDefinition == null) // not yet resolved
                  {
                     if (colRef.Qualifier == null)
                     {
                        // unqualified select: SELECT p FROM
                        resolveTypeForColumnReference(colRef);
                     }
                     else
                     {
                        // qualified select: SELECT t.p FROM
                        validateColumnReferenceAndResolveType(colRef);
                     }
                  }
               }
            }

            // Replace types used as qualifiers (IN_TREE, IN_FOLDER,
            // CONTAINS) by their corresponding alias (correlation name)
            foreach (KeyValuePair<int?, string> en in typeReferences)
            {
               int? obj = en.Key;
               string qualifier = en.Value;
               string typeQueryName = getReferencedTypeQueryName(qualifier);
               if (typeQueryName == null)
               {
                  throw new CmisQueryException(qualifier + " is neither a type query name nor an alias.");
               }
               if (typeQueryName.Equals(qualifier))
               {
                  // try to find an alias for it
                  string alias = null;
                  foreach (KeyValuePair<string, string> e in froms)
                  {
                     string q = e.Key;
                     string tqn = e.Value;
                     if (!tqn.Equals(q) && typeQueryName.Equals(tqn))
                     {
                        alias = q;
                        break;
                     }
                  }
                  if (alias != null)
                  {
                     typeReferences[obj] = alias;
                  }
               }
            }

            return true;
         }
         catch (CmisQueryException cqe)
         {
            errorMessage = cqe.Message; // preserve message
            return false; // indicate an error to ANTLR so that it generates FailedPredicateException
         }
      }

      protected internal virtual void resolveTypeForAlias(ColumnReference colRef)
      {
         string aliasName = colRef.AliasName;

         if (colOrFuncAlias.ContainsKey(aliasName))
         {
            CmisSelector selector = colOrFuncAlias[aliasName];
            if (selector is ColumnReference)
            {
               colRef = (ColumnReference)selector; // alias target
               if (colRef.Qualifier == null)
               {
                  // unqualified select: SELECT p FROM
                  resolveTypeForColumnReference(colRef);
               }
               else
               {
                  // qualified select: SELECT t.p FROM
                  validateColumnReferenceAndResolveType(colRef);
               }
            }
            // else --> ignore FunctionReference
         }
      }

      // for a select x from y, z ... find the type in type manager for x
      protected internal virtual void resolveTypeForColumnReference(ColumnReference colRef)
      {
         string propName = colRef.PropertyQueryName;
         bool isStar = propName.Equals("*");

         // it is property query name without a type, so find type
         int noFound = 0;
         TypeDefinition tdFound = null;
         foreach (string typeQueryName in froms.Values)
         {
            TypeDefinition td = typeMgr.GetTypeByQueryName(typeQueryName);
            if (null == td)
            {
               throw new CmisQueryException(typeQueryName + " is neither a type query name nor an alias.");
            }
            else if (isStar)
            {
               ++noFound;
               tdFound = null;
            }
            else if (typeMgr.TypeContainsPropertyWithQueryName(td, propName))
            {
               ++noFound;
               tdFound = td;
            }
         }
         if (noFound == 0)
         {
            throw new CmisQueryException(propName + " is not a property query name in any of the types in from ...");
         }
         else if (noFound > 1 && !isStar)
         {
            throw new CmisQueryException(propName + " is not a unique property query name within the types in from ...");
         }
         else
         {
            if (null != tdFound)
            {
               validateColumnReferenceAndResolveType(tdFound, colRef);
            }
         }
      }

      // for a select x.y from x ... check that x has property y and that x is in
      // from
      protected internal virtual void validateColumnReferenceAndResolveType(ColumnReference colRef)
      {
         // either same name or mapped alias
         string typeQueryName = getReferencedTypeQueryName(colRef.Qualifier);
         TypeDefinition td = typeMgr.GetTypeByQueryName(typeQueryName);
         if (null == td)
         {
            throw new CmisQueryException(colRef.Qualifier + " is neither a type query name nor an alias.");
         }

         validateColumnReferenceAndResolveType(td, colRef);
      }

      protected internal virtual void validateColumnReferenceAndResolveType(TypeDefinition td, ColumnReference colRef)
      {

         // type found, check if property exists
         bool hasProp=false;
         if (colRef.PropertyQueryName.Equals("*"))
         {
            hasProp = true;
         }
         else
         {
            hasProp = typeMgr.TypeContainsPropertyWithQueryName(td, colRef.PropertyQueryName);
         }
         if (!hasProp)
         {
            throw new CmisQueryException(colRef.PropertyQueryName + " is not a valid property query name in type " + td.Id + ".");
         }

         colRef.setTypeDefinition(typeMgr.GetPropertyIdForQueryName(td, colRef.PropertyQueryName), td);
      }

      // return type query name for a referenced column (which can be the name
      // itself or an alias
      protected internal virtual string getReferencedTypeQueryName(string qualifier)
      {
         string typeQueryName = froms[qualifier];
         if (null == typeQueryName)
         {
            // if an alias was defined but still the original is used we have to
            // search case: SELECT T.p FROM T AS TAlias
            string q = null;
            foreach (string tqn in froms.Values)
            {
               if (qualifier.Equals(tqn))
               {
                  if (q != null)
                  {
                     throw new CmisQueryException(qualifier + " is an ambiguous type query name.");
                  }
                  q = tqn;
               }
            }
            return q;
         }
         else
         {
            return typeQueryName;
         }
      }

   }
}
